﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.SdkTypeProxy.Metadata;
using Microsoft.Crm.Sdk.Metadata;
using mSpy.UI.Model;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Query;
using System.Threading;
using System.Net;
using System.Reflection;
using System.IO;
using System.Web.Services.Protocols;
using System.Text.RegularExpressions;

namespace mSpy.UI
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
            SetupBackgroundWorkers();
        }

        #region private variables
        private BackgroundWorker _getMessageWorker = new BackgroundWorker();
        private BackgroundWorker _hookWorker = new BackgroundWorker();

        private CrmSdkWrapper _sdk = null;
        private List<EntityMetadata> _entities = null;
        private List<mSpy.UI.Model.Message> _messages = null; // holds available crm messages we can hook into

        private Guid _solutionId = Guid.Empty;
        private Guid _error = new Guid("00000000-0000-0000-0000-000000000001");
        private SpyServer _server = null; // our small tcp/ip server that listens for events from our spy plugin
        #endregion

        private void SetupBackgroundWorkers()
        {
            _getMessageWorker.DoWork += new DoWorkEventHandler(_getMessageWorker_DoWork);
            _getMessageWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(_getMessageWorker_RunWorkerCompleted);

            _hookWorker.DoWork += new DoWorkEventHandler(_hookWorker_DoWork);
            _hookWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(_hookWorker_RunWorkerCompleted);
        }

        #region form event handlers
        private void MainForm_Load(object sender, EventArgs e)
        {
            ShowConnectionWindow();
        }

        private void connectToolStripMenuItem_Click(object sender, EventArgs e)
        {
            ShowConnectionWindow();
        }

        private void startSpying_Click(object sender, EventArgs e)
        {
            #region validation
            if (string.IsNullOrEmpty(this.iplist.Text))
            {
                MessageBox.Show("Please select an ipaddress which the crm server can call back on.", "Insufficient information", MessageBoxButtons.OK);
                return;
            }

            if (this.settingsview.Rows.Count - 1 <= 0)
            {
                MessageBox.Show("Please add at least 1 step to monitor.", "Insufficient information", MessageBoxButtons.OK);
                return;
            }
            #endregion

            List<SdkMessageProcessingStepRegistration> steps = new List<SdkMessageProcessingStepRegistration>();
            for (int i = 0; i < this.settingsview.Rows.Count - 1; i++) // ignore the last row since it's always blank
            {
                var current = this.settingsview.Rows[i];
                #region validation
                if (current.Cells[1].Value == null)
                {
                    MessageBox.Show(
                        string.Format("You must specify a message to monitor for row #{0}.", i + 1), "Insufficient information", MessageBoxButtons.OK);
                    return;
                }

                if (current.Cells[2].Value == null)
                {
                    MessageBox.Show(
                        string.Format("You must specify a stage to monitor for row #{0}.", i + 1), "Insufficient information", MessageBoxButtons.OK);
                    return;
                }

                if (current.Cells[3].Value == null)
                {
                    MessageBox.Show(
                        string.Format("You must specify a mode to monitor for row #{0}.", i + 1), "Insufficient information", MessageBoxButtons.OK);
                    return;
                }

                if (current.Cells[4].Value == null)
                {
                    MessageBox.Show(
                        string.Format("You must specify a pipeline to monitor for row #{0}.", i + 1), "Insufficient information", MessageBoxButtons.OK);
                    return;
                }
                #endregion

                var step = new SdkMessageProcessingStepRegistration
                {
                    CustomConfiguration = string.Format("{0}:{1}", this.iplist.Text, this.port.Value.ToString()),
                    Description = "mSpy #" + (i + 1), // just a unique identifier
                    Mode = (int)current.Cells[3].Value,
                    MessageName = current.Cells[1].Value.ToString(),
                    Stage = (int)current.Cells[2].Value,
                    SupportedDeployment = 0,
                    PluginTypeName = "mSpy.Plugins.Hook",
                    PluginTypeFriendlyName = "mSnoop Spy",
                    InvocationSource = (int)current.Cells[4].Value,
                };

                if (current.Cells[0].Value != null &&
                    !string.IsNullOrEmpty(current.Cells[0].Value.ToString()))
                {
                    step.PrimaryEntityName = current.Cells[0].Value.ToString();
                }

                steps.Add(step);
            }

            Initialize(steps);
        }

        private void stopSpying_Click(object sender, EventArgs e)
        {
            AppendLine("Removing hooks, please wait...");
            Unhook(this._sdk.Sdk);

            if (_server != null)
            {
                try
                {
                    _server.Kill();
                }
                catch { }
            }

            AppendLine("Successfully removed hooks");

            this.startSpying.Enabled = true;
            this.stopSpying.Enabled = false;
        }

        private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            stopSpying_Click(null, null);
        }
        #endregion

        #region background workers
        void _getMessageWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            this.messageColoumn.DisplayMember = "DisplayName";
            this.messageColoumn.DataSource = _messages;

            AppendLine(string.Format("Loaded {0} entities", _entities.Count));

            this.settingsview.Enabled = this.startSpying.Enabled = this.stopSpying.Enabled = true;
        }

        void _getMessageWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            _messages = mSpy.UI.Model.Message.GetAll(this._sdk.Sdk);
        }

        void Metadata_ExecuteCompleted(object sender, Microsoft.Crm.SdkTypeProxy.Metadata.ExecuteCompletedEventArgs e)
        {
            RetrieveAllEntitiesResponse metadata = (RetrieveAllEntitiesResponse)e.Result;
            _entities = metadata.CrmMetadata.OfType<EntityMetadata>().OrderBy(a => a.LogicalName).ToList();

            BindOptions();
        }

        void _hookWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (_solutionId != _error)
            {
                _server = new SpyServer(IPAddress.Parse(this.iplist.Text), (int)this.port.Value);
                _server.Message += new SpyServer.SpyEventHandler(server_Message);
                _server.Start();
                AppendLine("Successfully started the spy server");

                this.stopSpying.Enabled = true;
            }
            else
            {
                this.startSpying.Enabled = true;
                this.stopSpying.Enabled = false;
            }
        }

        void _hookWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            try
            {
                _solutionId = Hook(this._sdk.Sdk, (e.Argument as List<SdkMessageProcessingStepRegistration>));
                AppendLine("Successfully hooked into crm");
            }
            catch (SoapException se)
            {
                AppendLine("Unable to hook into crm, please check hooking steps/messages/stages/modes and try again");
                MessageBox.Show(se.Detail.InnerText, "Unexpected error");

                _solutionId = _error;
            }
        }

        void server_Message(object sender, SpyEvents e)
        {
            AppendLine("\r\n" + e.Message);
        }
        #endregion

        #region helper methods
        private void ShowConnectionWindow()
        {
            Views.Connect connection = new Views.Connect();
            if (connection.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                _sdk = connection.Sdk;
                this.connectionstatus.Text = string.Format("Successfully connected to: {0}", _sdk.Sdk.CrmAuthenticationTokenValue.OrganizationName);

                LoadData();
            }
        }

        private void LoadData()
        {
            AppendLine("Loading data, please wait...");

            RetrieveAllEntitiesRequest request = new RetrieveAllEntitiesRequest();
            request.MetadataItems = MetadataItems.EntitiesOnly;

            this._sdk.Metadata.ExecuteCompleted += new Microsoft.Crm.SdkTypeProxy.Metadata.ExecuteCompletedEventHandler(Metadata_ExecuteCompleted);
            this._sdk.Metadata.ExecuteAsync(request);
        }
        
        private void BindOptions()
        {
            _getMessageWorker.RunWorkerAsync();

            var ips = Dns.GetHostAddresses(Dns.GetHostName()).ToList().Where(ip => ip.ToString().Contains(".")).ToList(); // only ipv4 is supported
            this.iplist.DataSource = ips;

            this.entityNameColumn.DisplayMember = "LogicalName";
            this.entityNameColumn.DataSource = _entities;

            this.stageColumn.DisplayMember = "DisplayName";
            this.stageColumn.ValueMember = "Value";
            this.stageColumn.DataSource = Stage.GetAll();

            this.modeColumn.DisplayMember = "DisplayName";
            this.modeColumn.ValueMember = "Value";
            this.modeColumn.DataSource = Mode.GetAll();

            this.pipelineColumn.DisplayMember = "DisplayName";
            this.pipelineColumn.ValueMember = "Value";
            this.pipelineColumn.DataSource = Pipeline.GetAll();
        }

        private void AppendLine(string msg)
        {
            if (this.spylog.InvokeRequired)
            {
                this.spylog.BeginInvoke(new MethodInvoker(delegate()
                {
                    AppendColorize(msg);
                }));
            }
            else
            {
                AppendColorize(msg);
            }
        }

        private void AppendColorize(string msg)
        {
            msg = string.Format("{0:dd-MM-yy HH:mm}: {1}\r\n", DateTime.Now, msg);
            int start = this.spylog.Text.Length;

            this.spylog.AppendText(msg);

            Regex re = new Regex(": (.*) : (.*)(\n|\r|\r\n)");
            Color type = Color.Blue;
            Color param = Color.Red;

            foreach (Match match in re.Matches(this.spylog.Text.Substring(start)))
            {
                if (match.Groups.Count >= 3)
                {
                    Colorize(match.Groups[1].Index + start, match.Groups[1].Length, type);
                    Colorize(match.Groups[2].Index + start, match.Groups[2].Length, param);
                }

                if (match.Groups.Count >= 2 && match.Groups.Count <= 3)
                {
                    Colorize(match.Groups[1].Index + start, match.Groups[1].Length, param);
                }
            }

            this.spylog.Select(this.spylog.Text.Length, 0); // always scroll to the bottom
        }

        private void Colorize(int start, int length, Color color)
        {
            this.spylog.Select(start, length);
            this.spylog.SelectionColor = color;
        }

        private void Initialize(List<SdkMessageProcessingStepRegistration> steps)
        {
            this.startSpying.Enabled = false;
            this.stopSpying.Enabled = false;

            AppendLine("Hooking into crm, please wait...");

            _hookWorker.RunWorkerAsync(steps);
        }

        private Guid Hook(CrmService sdk, List<SdkMessageProcessingStepRegistration> steps)
        {
            Unhook(sdk);

            string asmPath = Path.Combine(
                Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "mSpy.Plugins.dll");

            RegisterSolutionRequest request = new RegisterSolutionRequest
            {
                PluginAssembly = GetPluginAssembly(asmPath),
                Steps = steps.ToArray()
            };

            RegisterSolutionResponse response = sdk.Execute(request) as RegisterSolutionResponse;
            return response.PluginAssemblyId;
        }

        private void Unhook(CrmService sdk)
        {
            try
            {
                QueryExpression qe = new QueryExpression
                {
                    EntityName = "pluginassembly",
                    ColumnSet = new ColumnSet(new string[] { "pluginassemblyid" }),
                };
                qe.Criteria.AddCondition("name", ConditionOperator.Like, "%mSpy%");

                var result = sdk.Execute(new RetrieveMultipleRequest
                {
                    Query = qe,
                    ReturnDynamicEntities = true
                }) as RetrieveMultipleResponse;

                if (result != null && result.BusinessEntityCollection != null &&
                    result.BusinessEntityCollection.BusinessEntities != null &&
                    result.BusinessEntityCollection.BusinessEntities.Count > 0)
                {
                    sdk.Execute(new UnregisterSolutionRequest
                    {
                        PluginAssemblyId =
                        ((result.BusinessEntityCollection.BusinessEntities[0] as DynamicEntity)["pluginassemblyid"] as Key).Value
                    });
                }
            }
            catch { } // who cares if it fails
        }

        private pluginassembly GetPluginAssembly(string path)
        {
            var assembly = Assembly.LoadFile(path);
            string[] prop = assembly.GetName().FullName.Split(",= ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

            pluginassembly asm = new pluginassembly
            {
                name = prop[0],
                culture = prop[4],
                version = prop[2],
                publickeytoken = prop[6],
                sourcetype = new Picklist(AssemblySourceType.Database),
                content = Convert.ToBase64String(File.ReadAllBytes(path))
            };

            return asm;
        }
        #endregion
    }
}
